package datastore

import (
	"context"

	"github.com/stackrox/rox/central/imageintegration/store"
	v1 "github.com/stackrox/rox/generated/api/v1"
	"github.com/stackrox/rox/generated/storage"
	"github.com/stackrox/rox/pkg/env"
	"github.com/stackrox/rox/pkg/features"
	"github.com/stackrox/rox/pkg/openshift"
	"github.com/stackrox/rox/pkg/sac"
	"github.com/stackrox/rox/pkg/sac/resources"
	searchPkg "github.com/stackrox/rox/pkg/search"
	"github.com/stackrox/rox/pkg/uuid"
)

var (
	integrationSAC = sac.ForResource(resources.Integration)
)

type datastoreImpl struct {
	storage store.Store
}

func (ds *datastoreImpl) Count(ctx context.Context, q *v1.Query) (int, error) {
	return ds.storage.Count(ctx, q)
}

func (ds *datastoreImpl) Search(ctx context.Context, q *v1.Query) ([]searchPkg.Result, error) {
	return ds.storage.Search(ctx, q)
}

// GetImageIntegration is pass-through to the underlying store.
func (ds *datastoreImpl) GetImageIntegration(ctx context.Context, id string) (*storage.ImageIntegration, bool, error) {
	if ok, err := integrationSAC.ReadAllowed(ctx); err != nil {
		return nil, false, err
	} else if !ok {
		return nil, false, nil
	}

	return ds.storage.Get(ctx, id)
}

// GetImageIntegrations provides an in memory layer on top of the underlying DB based storage.
func (ds *datastoreImpl) GetImageIntegrations(ctx context.Context, request *v1.GetImageIntegrationsRequest) ([]*storage.ImageIntegration, error) {
	if ok, err := integrationSAC.ReadAllowed(ctx); err != nil {
		return nil, err
	} else if !ok {
		return nil, nil
	}

	if request.GetCluster() != "" {
		return nil, nil
	}

	var integrationSlice []*storage.ImageIntegration
	err := ds.storage.Walk(ctx, func(integration *storage.ImageIntegration) error {
		if request.GetName() == "" || request.GetName() == integration.GetName() {
			integrationSlice = append(integrationSlice, integration)
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return integrationSlice, nil
}

// AddImageIntegration is pass-through to the underlying store.
func (ds *datastoreImpl) AddImageIntegration(ctx context.Context, integration *storage.ImageIntegration) (string, error) {
	if ok, err := integrationSAC.WriteAllowed(ctx); err != nil {
		return "", err
	} else if !ok {
		return "", sac.ErrResourceAccessDenied
	}

	sourcedIntegration := features.SourcedAutogeneratedIntegrations.Enabled() ||
		(env.AutogenerateGlobalPullSecRegistries.BooleanSetting() && openshift.GlobalPullSecretIntegration(integration))

	// Use the provided ID for sourced integrations, trust that the ID is
	// predictable for reconciliation, deletes, etc. to work.
	if !sourcedIntegration || integration.GetId() == "" {
		integration.Id = uuid.NewV4().String()
	}
	err := ds.storage.Upsert(ctx, integration)
	if err != nil {
		return "", err
	}
	return integration.GetId(), nil
}

// UpdateImageIntegration is pass-through to the underlying store.
func (ds *datastoreImpl) UpdateImageIntegration(ctx context.Context, integration *storage.ImageIntegration) error {
	if ok, err := integrationSAC.WriteAllowed(ctx); err != nil {
		return err
	} else if !ok {
		return sac.ErrResourceAccessDenied
	}

	return ds.storage.Upsert(ctx, integration)
}

// RemoveImageIntegration is pass-through to the underlying store.
func (ds *datastoreImpl) RemoveImageIntegration(ctx context.Context, id string) error {
	if ok, err := integrationSAC.WriteAllowed(ctx); err != nil {
		return err
	} else if !ok {
		return sac.ErrResourceAccessDenied
	}
	return ds.storage.Delete(ctx, id)
}

// SearchImageIntegrations
func (ds *datastoreImpl) SearchImageIntegrations(ctx context.Context, q *v1.Query) ([]*v1.SearchResult, error) {
	// TODO(ROX-29943): remove 2 pass database calls
	results, err := ds.storage.Search(ctx, q)
	if err != nil {
		return nil, err
	}
	var imageIntegrationList []*storage.ImageIntegration
	var trimmedResultsList []searchPkg.Result
	for _, result := range results {
		singleImageIntegration, exists, err := ds.storage.Get(ctx, result.ID)
		if err != nil {
			return nil, err
		}
		// The result may not exist if the object was deleted after the search
		if !exists {
			continue
		}
		imageIntegrationList = append(imageIntegrationList, singleImageIntegration)
		// To ensure the records match up we need a sublist of results in case the 2 pass call missed any
		trimmedResultsList = append(trimmedResultsList, result)
	}

	protoResults := make([]*v1.SearchResult, 0, len(imageIntegrationList))
	for i, imageIntegration := range imageIntegrationList {
		protoResults = append(protoResults, convertImageIntegration(imageIntegration, trimmedResultsList[i]))
	}
	return protoResults, nil
}

// convertImageIntegration returns proto search result from a image integration object and the internal search result
func convertImageIntegration(imageIntegration *storage.ImageIntegration, result searchPkg.Result) *v1.SearchResult {
	return &v1.SearchResult{
		Category:       v1.SearchCategory_IMAGE_INTEGRATIONS,
		Id:             imageIntegration.GetId(),
		Name:           imageIntegration.GetName(),
		FieldToMatches: searchPkg.GetProtoMatchesMap(result.Matches),
		Score:          result.Score,
	}
}
